package net;

import gui.BoardGUI;
import gui.Gui;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Random;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import logic.Team;
import messages.CheckGameState;
import messages.GameEventDispatcher;
import messages.GameEventListener;
import messages.GameMessage;
import messages.MovePiece;
import messages.ConnectionEstablished;
import messages.PlayerDisconnected;
import messages.RotatePiece;
import messages.SetBoardState;
import messages.UmountObelisk;

public class GameClient implements GameEventDispatcher, GameEventListener{
	private ArrayList<GameEventListener> listeners;
	private DatagramSocket socket;
	private InetAddress serverAddr;
	private int port;
	private BoardGUI board;
	
	private String playerName = "test.client";
	
	private String serverName;
	private Team serverPlayer;
	private Team player;		/*TODO
	 * Process messages
	 */
	private boolean playing = false;
	private KeepAliveDaemon keepAlive;
	
	private String lastServerState;
	
	private Hashtable<String, Document> msgSent = new Hashtable<String, Document>();
	private Hashtable<Long, String> timeToMsgId = new Hashtable<Long, String>();
	
	
	public GameClient(String host, int port, BoardGUI board, GameEventListener listener) throws IOException{
		this.board = board;
		serverAddr = InetAddress.getByName(host);
		this.port = port;
		socket = new DatagramSocket();
		socket.setSoTimeout(30*1000);
		registerGameListener(listener);
		if(doHandshake()){
			playing = true;
			keepAlive = new KeepAliveDaemon(socket, serverAddr, port);
			keepAlive.start();
		}else{
			closeConnection();
		}
		
		Thread receiveMessagesFromServer = new Thread(new Runnable() {
			
			@Override
			public void run() {
				DatagramPacket packet;
				while(playing){
					byte[] buf = new byte[15*1024];
					packet = new DatagramPacket(buf, buf.length);
					try {
						socket.setSoTimeout(30*1000);
						socket.receive(packet);
						if(packet.getAddress().equals(serverAddr)&&packet.getPort()==GameClient.this.port)
							parseMessage(packet);
					}catch(SocketTimeoutException e){
						ConnectionTimeout toMsg = new ConnectionTimeout();
						try {
							socket.send(toMsg.getMsgPacket(serverAddr, GameClient.this.port));
							packet = new DatagramPacket(buf, buf.length);
							socket.setSoTimeout(5*1000);
							socket.receive(packet);
							if(playing)
								if(!packet.getAddress().equals(serverAddr)&&packet.getPort()!=GameClient.this.port)
									closeConnection();
						}catch (Exception e1) {
							System.out.println("Server "+serverName+" has disconnected. (Connection Timeout)");
							closeConnection();
						}
					}catch (IOException e) {
						e.printStackTrace();
					}
					
				}
			}
		});
		receiveMessagesFromServer.start();
		sendEvent(new ConnectionEstablished(serverName, serverPlayer, player));
	}
	
	private boolean doHandshake() throws IOException{
		HandshakeMsg handshakeMsg = new HandshakeMsg(playerName);
		try {
			DatagramPacket hsPacket = handshakeMsg.getMsgPacket(serverAddr, port);
			socket.send(hsPacket);
			DatagramPacket response = new DatagramPacket(new byte[1500], 1500);
			socket.setSoTimeout(10*1000);
			socket.receive(response);
			HandshakeMsg responseHandshake = HandshakeMsg.parseDatagram(response);
			serverName = responseHandshake.getPlayer();
			serverPlayer = responseHandshake.getColor();
			switch (serverPlayer) {
			case RED:
				player = Team.GRAY;
				break;
			case GRAY:
				player = Team.RED;
			}
			String gameState = responseHandshake.getMD5State();
			if(sendEventGUI(new CheckGameState(gameState))){
				// set the player's team
			}else{
				Document serverState = gimmiServerState();
				sendEventGUI(new SetBoardState(serverState));
			}
			return true;
		} catch (IOException e) {
			closeConnection();
			throw e;
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		}
		return false;
	}
	
	public void closeConnection(){
		CloseConnection msgClose = new CloseConnection();
		DatagramPacket closePacket = msgClose.getMsgPacket(serverAddr, port);
		closePacket.setAddress(serverAddr);
		closePacket.setPort(port);
		try {
			socket.send(closePacket);
		} catch (IOException e) {
			/*
			 * Server already disconnected
			 */
		}
		serverAddr = null;
		playing = false;
		port = 0;
		if(keepAlive!=null)
			keepAlive.stopIt();
		socket.close();
		sendEvent(new PlayerDisconnected(serverName));
	}

	@Override
	public void registerGameListener(GameEventListener listener) {
		if(listeners==null)
			listeners = new ArrayList<GameEventListener>();
		listeners.add(listener);
		
	}

	private boolean sendEventGUI(GameMessage message){
		if(listeners==null)
			return false;
		for(GameEventListener listener:listeners){
			if(listener instanceof Gui){
				return listener.gameEvent(message);
			}
		}
		return false;
	}
	
	private Document gimmiServerState() throws IOException, SAXException{
		GimmiState serverGimmiState = new GimmiState();
		socket.send(serverGimmiState.getMsgPacket(serverAddr, port));
		DatagramPacket serverBoardConfigPckt = new DatagramPacket(new byte[10*1024], 10*1024);
		socket.setSoTimeout(10*1000);
		int retry=0;
		do{
			socket.receive(serverBoardConfigPckt);
			retry++;
		}while(!serverBoardConfigPckt.getAddress().equals(serverAddr)
				||serverBoardConfigPckt.getPort()!=port||retry>9);
		
		String serverStateXML = new String(serverBoardConfigPckt.getData()).trim();
        InputSource is = new InputSource();
        is.setCharacterStream(new StringReader(serverStateXML));
		Document message = newDocumentBuilder().parse(is);
		message.normalize();
		
		NodeList elements = message.getElementsByTagName(GameNetMessages.khetMsg);
		Node boardConf = null;
		for(int i=0;i<elements.getLength();i++){
			Element root = (Element)elements.item(i);
			if(root.getAttribute("type").equals(GameNetMessages.serverState)){
				boardConf = root.getFirstChild();
			}
		}
		if(boardConf==null)
			return null;
		Document boardDoc = newDocument();
		Node boardConftoDoc = boardDoc.importNode(boardConf, true);
		boardDoc.appendChild(boardConftoDoc);
		
		return boardDoc;
		
	}
	
	
	private Document newDocument(){
		try {
			return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
		return null;
	}
	
	private DocumentBuilder newDocumentBuilder(){
		try {
			return DocumentBuilderFactory.newInstance().newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
		return null;
	}
	
	private void parseMessage(DatagramPacket packet){
		String message = new String(packet.getData()).trim();
        InputSource is = new InputSource();
        is.setCharacterStream(new StringReader(message));
        System.out.println(System.currentTimeMillis()+" " +message);
        Document doc;
		try {
			doc = newDocumentBuilder().parse(is);
	        doc.normalize();
	        NodeList msgElements = doc.getElementsByTagName(GameNetMessages.khetMsg);
	        for(int i=0;i<msgElements.getLength();i++){
	        	Element khetMsg;
	        	if(!(msgElements.item(i) instanceof Element))
	        		return;
	        	khetMsg = (Element)msgElements.item(i);
	        	String msgType = khetMsg.getAttribute("type");
        		String msgId = khetMsg.getAttribute("id");
	        	if(msgType.equals(GameNetMessages.connectionTimeout)){
	        		KeepAlive ka = new KeepAlive();
	        		socket.send(ka.getMsgPacket(serverAddr, port));
	        		return;
	        	}
	        	if(msgType.equals(GameNetMessages.close)){
	        		closeConnection();
	        		return;
	        	}
	        	if(msgType.equals(GameNetMessages.move)){
					String x1msg = khetMsg.getAttribute("x1");
					String y1msg = khetMsg.getAttribute("y1");
					String x2msg = khetMsg.getAttribute("x2");
					String y2msg = khetMsg.getAttribute("y2");
					if(x1msg==null||y1msg==null||x2msg==null||y2msg==null)
						continue;
					try{
						int x1 = Integer.parseInt(x1msg);
						int y1 = Integer.parseInt(y1msg);
						int x2 = Integer.parseInt(x2msg);
						int y2 = Integer.parseInt(y2msg);
						sendEvent(new MovePiece(x1, y1, x2, y2, serverPlayer));
						sendACK(GameNetMessages.move, msgId);
					}catch(NumberFormatException e){}
	        	}
				if(msgType.equals(GameNetMessages.rotate)){
					String xmsg = khetMsg.getAttribute("x");
					String ymsg = khetMsg.getAttribute("y");
					String direction = khetMsg.getAttribute("direction");
					if(xmsg==null||ymsg==null||direction==null)
						continue;
					try{
						int x = Integer.parseInt(xmsg);
						int y = Integer.parseInt(ymsg);
						sendEvent(new RotatePiece(x, y, direction,serverPlayer));
						sendACK(GameNetMessages.rotate, msgId);
					}catch(NumberFormatException e){}
					return;
				}
	        	if(msgType.equals(GameNetMessages.umountObelisk)){
					String x1msg = khetMsg.getAttribute("x1");
					String y1msg = khetMsg.getAttribute("y1");
					String x2msg = khetMsg.getAttribute("x2");
					String y2msg = khetMsg.getAttribute("y2");
					if(x1msg==null||y1msg==null||x2msg==null||y2msg==null)
						continue;
					try{
						int x1 = Integer.parseInt(x1msg);
						int y1 = Integer.parseInt(y1msg);
						int x2 = Integer.parseInt(x2msg);
						int y2 = Integer.parseInt(y2msg);
						sendEvent(new UmountObelisk(x1, y1, x2, y2, serverPlayer));
						sendACK(GameNetMessages.umountObelisk, msgId);
					}catch(NumberFormatException e){}
					return;
	        	}if(msgType.equals(GameNetMessages.ack)){
	        		String action = khetMsg.getAttribute("value");
	        		String state = khetMsg.getAttribute("gameState");
	        		if(state.equals(lastServerState)){
	        			/* server outdated
	        			 * send again the message?
	        			 */
	        		}
	        		lastServerState = state;
	        		if(state.equals(board.gameState()))
	        			return;
	        		System.out.println("Remote player is not synchronized after action ["+action+"] msgid ="+msgId);
	        		Document remoteState = gimmiServerState();
	        		sendEventGUI(new SetBoardState(remoteState));
	        	}
	        }
			/*TODO
			 * Process messages
			 */
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			/*
			 * Close connection
			 */
			closeConnection();
			e.printStackTrace();
		}

	}
	

	
	public void sendMsgToServer(Document msg) throws TransformerFactoryConfigurationError, TransformerException, IOException{
		Transformer transformer = TransformerFactory.newInstance().newTransformer();
		transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
		transformer.setOutputProperty(OutputKeys.INDENT, "no");

		StreamResult result = new StreamResult(new StringWriter());
		Element khetMsg = (Element)msg.getFirstChild();
		Long msgId = new Random().nextLong();
		khetMsg.setAttribute("id", msgId+"");
		DOMSource source = new DOMSource(msg);
		transformer.transform(source, result);
		
		byte[] buf = result.getWriter().toString().getBytes();
		DatagramPacket packet = new DatagramPacket(buf, buf.length, serverAddr, port);
		socket.send(packet);
		putMessageInTable(msgId+"", msg);

	}
	
	private synchronized void putMessageInTable(String msgId, Document msg){
	//	msgSent.put(msgId, msg);
	//	timeToMsgId.put(System.currentTimeMillis(), msgId);
	}
	
	@SuppressWarnings({ "unchecked", "unused" })
	private synchronized void clearMessagesInTable(){
		long thisTime = System.currentTimeMillis();
		long tableMaxTime = thisTime-5*60*1000;
		for(Long currentTime:((Hashtable<Long, String>)timeToMsgId.clone()).keySet()){
			if(currentTime<tableMaxTime){
				timeToMsgId.remove(currentTime);
			}
		}
	}
	
	private void sendACK(String value, String transaction){
		Document msgDoc;
		try {
			msgDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
			msgDoc.normalize();
			Element msg = msgDoc.createElement(GameNetMessages.khetMsg);
			msgDoc.appendChild(msg);
			msg.setAttribute("type", GameNetMessages.ack);
			msg.setAttribute("value", value);
			msg.setAttribute("transaction", transaction);
			String boardState = board.gameState();
			msg.setAttribute("gameState", boardState);
			sendMsgToServer(msgDoc);
		} catch (ParserConfigurationException e1) {
			e1.printStackTrace();
		} catch (TransformerFactoryConfigurationError e) {
			e.printStackTrace();
		} catch (TransformerException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	
	@Override
	public void sendEvent(GameMessage event) {
		if(listeners==null)
			return;
		for(GameEventListener listener:listeners){
			listener.gameEvent(event);
		}
	}

	@Override
	public boolean gameEvent(GameMessage message) {
		if(message instanceof MovePiece){
			/*
			 * TODO
			 */
			return true;
		}
		return false;
		
	}

}
